using System;
using Evaluant.Uss.Commands;
//using Evaluant.Uss.Common;
using System.Threading;
using System.Collections;
using System.Diagnostics;
using Evaluant.Uss.PersistenceEngine.Contracts;
using Evaluant.Uss.Domain;

namespace Evaluant.Uss.Cache
{
    /// <summary>
    /// IPersistenceEngine which acts has a cache for another IPersistenceEngine. A Delegator must be specified.
    /// All the object are "weakly" kept into memory until the garbage collector is to collect them. Objects
    /// queried by Id are directly taken from memory if they can be found. On the contrary the OPath request is 
    /// processed in the Delegator and the object are collected into the cache.
    /// 
    /// In the main list, entities are kept with all the attributes and all the loaded references. The loaded references 
    /// contain only the entries, without the attributes nor the references. Its just a cache of pointed items. Those items
    /// can then be loaded from the main list no to have to load them from the delegator.
    /// 
    /// Caches indexes can't be contained statically by the Cache class as several ObjectContext from different 
    /// ObjectService (ie different repositories) would then use the same references. The solution is to let those references
    /// in the ObjectService instance which is unique by repository.
    /// </summary>
    public class CacheEngine : PersistenceEngineImplementation
    {
        // private static PerformanceCounter _Counter;

        private IdentityMap _Entities;
        private Hashtable _Scalars;
        private Hashtable _Loads;
        private ReaderWriterLock _RWL;

        static CacheEngine()
        {
            // _Counter = new PerformanceCounter(CacheProvider.CATEGORY, CacheProvider.NAME, "Value", false);
        }

        public CacheEngine(IPersistenceEngine delegator, IdentityMap entities, Hashtable scalars, Hashtable loads, ReaderWriterLock rwl)
        {
            _CommandProcessor = new CacheCommandProcessor(entities, rwl);
            _Delegator = delegator;
            _Entities = entities;
            _Scalars = scalars;
            _Loads = loads;
            _RWL = rwl;
        }

        ICommandProcessor _CommandProcessor;
        public ICommandProcessor CommandProcessor
        {
            get { return _CommandProcessor; }
            set { _CommandProcessor = value; }
        }

        private IPersistenceEngine _Delegator;
        public IPersistenceEngine Delegator
        {
            get { return _Delegator; }
            set { _Delegator = value; }
        }

        public void EnsureDelegator()
        {
            if (_Delegator == null)
                throw new Exception("A delegated Persistence Engine must be specified for the Cache Persistence Engine");
        }

        public override EntitySet Load(NLinqVisitors opath, string[] attributes, string orderby, int first, int max)
        {
            _RWL.AcquireReaderLock(Timeout.Infinite);

            EntitySet entityset = _Delegator.Load(opath, new string[0], orderby, first, max);
            UpdateMap(entityset.ToArray());

            try
            {
                // Removes unwanted attributes
                if (attributes == null || attributes.Length > 0)
                    foreach (Entity e in entityset)
                        for (int i = e.EntityEntries.Count - 1; i >= 0; i--)
                        {
                            EntityEntry ee = e.EntityEntries[i] as EntityEntry;
                            if (!ee.IsEntity && (attributes == null || Array.IndexOf(attributes, ee.Name) == -1))
                                e.RemoveAttribute(ee.Name);
                        }

                return entityset;
            }
            finally
            {
                _RWL.ReleaseReaderLock();
            }
        }

        public override void LoadReference(System.Collections.IEnumerable entities, string[] references)
        {
            _RWL.AcquireReaderLock(Timeout.Infinite);

            try
            {
                foreach (Entity entity in entities)
                {
                    State tempState = entity.State;

                    entity.RemoveReference(references);

                    Entity local = _Entities.GetValue(Utils.GetCacheKey(entity));

                    if (local != null)
                    {
                        ArrayList unknownIds = new ArrayList();

                        // _Counter.Increment();

                        /// TODO: Create only one call to the Delegator by specifying the list of the references to load
                        foreach (string name in references)
                        {
                            // Tries to get the references from the cache
                            if (local.GetValue(name) != null)
                            {
                                unknownIds.Clear();

                                // Adds all the entities which are still in the cache
                                foreach (Entity e in local.GetEntitySet(name))
                                {
                                    Entity eLocal = _Entities.GetValue(Utils.GetCacheKey(e));
                                    if (eLocal != null)
                                        entity.AddValue(name, eLocal.Clone(new string[0]), State.UpToDate);
                                    else
                                        unknownIds.Add(e.Id);
                                }

                                // Requests all the unknown ids to the delegator
                                if (unknownIds.Count > 0)
                                {
                                    string[] ids = (string[])unknownIds.ToArray(typeof(string));
                                    EntitySet unknowns = _Delegator.LoadWithId(_Model.GetReference(local.Type, name, true).ChildType, ids);
                                    foreach (Entity unknown in unknowns)
                                        entity.AddValue(name, unknown.Clone(new string[0]), State.UpToDate);

                                    UpdateMap(unknowns.ToArray());
                                }

                            }
                            else
                                _Delegator.LoadReference(new Entity[] { entity }, new string[] { name });

                            entity.State = tempState;
                        }
                    }
                    else
                    {
                        /// TODO: If all the entities don't have the reference, use a call to the collection of entities
                        _Delegator.LoadReference(entity, references);
                        UpdateMap(new Entity[] { entity });
                    }

                    // mark the reference as loaded for each parentEntity
                    foreach (string refName in references)
                        if (!entity.InferredReferences.Contains(refName))
                            entity.InferredReferences.Add(refName);
                }
            }
            finally
            {
                _RWL.ReleaseReaderLock();
            }
        }

        public override EntitySet LoadWithId(string type, string[] ids, string[] attributes)
        {
            _RWL.AcquireReaderLock(Timeout.Infinite);

            try
            {
                EntitySet result = new EntitySet();

                foreach (string id in ids)
                {
                    Entity local = _Entities.GetValue(Utils.GetCacheKey(type, id));
                    if (local != null)
                    {
                        // _Counter.Increment();

                        Entity entity = local.Clone(attributes);

                        entity.MetaData = Entity.LoadMetaData.AttributesLoaded;
                        entity.State = State.UpToDate;
                        result.Add(entity);
                    }
                    else
                    {
                        /// TODO: Optimize so that only the entities that are not in cache are loaded from the delegator

                        EntitySet es = _Delegator.LoadWithId(type, new string[] { id }, new string[0]);
                        UpdateMap(es.ToArray());

                        // Removes unwanted attributes
                        if (attributes == null || attributes.Length > 0)
                            foreach (Entity e in es)
                            {
                                for (int i = e.EntityEntries.Count - 1; i >= 0; i--)
                                {
                                    EntityEntry ee = e.EntityEntries[i] as EntityEntry;
                                    if (!ee.IsEntity && (attributes == null || Array.IndexOf(attributes, ee.Name) == -1))
                                        e.RemoveAttribute(ee.Name);
                                }
                                e.State = State.UpToDate;
                            }

                        foreach (Entity e in es)
                            result.Add(e);
                    }
                }

                return result;
            }
            finally
            {
                _RWL.ReleaseReaderLock();
            }
        }

        /// <remarks>
        /// Identical queries are not processed again
        /// </remarks>
        public override object LoadScalar(string opath)
        {
            lock (_Scalars)
            {
                if (_Scalars.Contains(opath))
                    return _Scalars[opath];

                object scalar = _Delegator.LoadScalar(opath);
                _Scalars.Add(opath, scalar);

                return scalar;
            }
        }

        public override object LoadScalar(Evaluant.OPath.OPathQuery opath)
        {
            lock (_Scalars)
            {
                if (_Scalars.Contains(opath.Expression.ToString()))
                    return _Scalars[opath.Expression.ToString()];

                object scalar = _Delegator.LoadScalar(opath);
                _Scalars.Add(opath, scalar);

                return scalar;
            }
        }

        public override void BeforeProcessCommands(Transaction tx)
        {
            _RWL.AcquireWriterLock(Timeout.Infinite);

            try
            {
                _Delegator.BeforeProcessCommands(tx);
            }
            catch
            {
                // Releases the lock if an error occured
                _RWL.ReleaseWriterLock();
                throw;
            }
        }

        public override void ProcessCommands(Transaction tx)
        {
            try
            {
                _Delegator.ProcessCommands(tx);

                foreach (Command command in tx.PendingCommands)
                    command.Accept(_CommandProcessor);

                // Invalidates the Scalar cache
                _Scalars.Clear();
            }
            catch
            {
                // Releases the lock if an error occured
                _RWL.ReleaseWriterLock();
                throw;
            }
        }

        public override void AfterProcessCommands(Transaction tx)
        {
            try
            {
                _Delegator.AfterProcessCommands(tx);
            }
            finally
            {
                // Releases the lock if an error occured
                _RWL.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Adds the clone of an entityset to the cache entries
        /// </summary>
        /// <param name="entityset">The entityset to clone</param>
        private void UpdateMap(Entity[] entities)
        {
            lock (_Entities)
            {
                RecursiveUpdateMap(entities);
            }
        }

        private void RecursiveUpdateMap(Entity[] entities)
        {
            foreach (Entity e in entities)
            {
                Entity local = _Entities.GetValue(Utils.GetCacheKey(e));

                // If the entity was found there is nothing to do
                if (local == null)
                {
                    Entity clone = e.Clone(new string[0]);

                    _Entities.Add(Utils.GetCacheKey(clone), clone);

                    foreach (EntityEntry ee in e)
                    {
                        if (ee.IsEntity)
                        {
                            Entity sub = (Entity)ee.Value;
                            // Adds the entries to references
                            clone.AddValue(ee.Name, sub.Clone(), State.UpToDate);
                            RecursiveUpdateMap(new Entity[] { sub });
                        }
                    }

                    clone.State = State.UpToDate;
                }
            }
        }

        public override void InitializeRepository()
        {
            _RWL.AcquireWriterLock(Timeout.Infinite);
            try
            {
                EnsureDelegator();
                _Delegator.InitializeRepository();
            }
            finally
            {
                _RWL.ReleaseWriterLock();
            }

            Initialize();
        }

        public override void Initialize()
        {
            _RWL.AcquireWriterLock(Timeout.Infinite);
            try
            {
                EnsureDelegator();
                _Delegator.Initialize();
                _Entities.Clear();
            }
            finally
            {
                _RWL.ReleaseWriterLock();
            }

        }
    }
}
